Feature/#88 notification page link feat#104
Conversation
📝 WalkthroughWalkthroughJob-posting analysis now distinguishes saved and unsaved results, notification handling uses authenticated SSE with refreshed API data and toast feedback, and resume-analysis failures propagate through query parameters into a non-retrying notice flow. ChangesNotification contract and stream
Notification UI and routing
Job-posting analysis flow
Resume-analysis error flow
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
jobdri/src/lib/api/notification.ts (1)
91-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the commented-out debug block.
♻️ Proposed cleanup
onmessage(event) { - // console.log( - // "📥 [SSE 통신 감지!!] 서버가 보낸 원본 데이터:", - // event.data, - // ); - if (!event.data || !event.data.trim().startsWith("{")) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/lib/api/notification.ts` around lines 91 - 95, Remove the commented-out console.log debug block near the SSE event handling code, leaving the surrounding notification logic unchanged.jobdri/src/components/common/lnb/LnbNotification.tsx (1)
428-436: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
taskIdis destructured but never used in the switch below — drop it to avoid an unused-variable lint failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/components/common/lnb/LnbNotification.tsx` around lines 428 - 436, Remove taskId from the notificationItem destructuring in LnbNotification so only fields used by the switch and surrounding logic remain.jobdri/src/components/common/lnb/Lnb.tsx (2)
168-169: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winToast timer is never cleared.
Back-to-back notifications stack timers, so an earlier timeout can dismiss a newer toast, and nothing cancels the pending timer on unmount. Store the handle in a ref and clear it before scheduling and in the effect cleanup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/components/common/lnb/Lnb.tsx` around lines 168 - 169, Update the toast state flow in Lnb to store the setTimeout handle in a ref, clear any existing timer before scheduling a new dismissal, and clear the pending timer during effect cleanup on unmount. Preserve the current 3000ms dismissal behavior while preventing older notifications from dismissing newer toasts.
108-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
hasSubscribedRefguard doesn't achieve its stated goal.The cleanup resets the ref to
false, so the Strict Mode double-mount it's meant to block will still re-subscribe on the second mount. With an empty dep array the effect already runs once per mount; consider dropping the ref, or keep it module-scoped if you truly need a single global stream per session.Also applies to: 186-189
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/components/common/lnb/Lnb.tsx` around lines 108 - 110, The hasSubscribedRef guard in the Lnb useEffect does not prevent Strict Mode re-subscription because cleanup resets it. Remove the ref guard and its reset, relying on the empty dependency array for one subscription per mount, or move the guard to module scope only if a single stream must be shared globally.jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx (1)
269-298: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
handleRetryappears orphaned after collapsing the modal to a single action.The modal (Lines 289-298) now only wires a single "확인" action to
moveBackToResume;handleRetry(resetserrorMessage/pollingRetryKey) is no longer referenced by any control in the render. If dropping the retry path for all error states (not just the timeout/isErrorcase) is intentional, removehandleRetry; otherwise re-wire it as a secondary action for the recoverable error paths (e.g., transient polling failures) while keeping the single-action modal for theisError/timeout case.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/app/mockApply/`[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx around lines 269 - 298, Resolve the orphaned handleRetry function in the ResumeAnalysisLoadingPageClient render flow: either remove handleRetry and its retry state usage if all error states intentionally exit through moveBackToResume, or wire it as a secondary action only for recoverable transient polling failures while preserving the single-action modal for the isError/timeout case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@jobdri/src/app/mockApply/`[mockApplyId]/(jd)/jd-review/page.tsx:
- Around line 31-97: Remove the accidentally added subscribeToNotificationStream
helper and its SSE-related imports or references from this page. Restore a local
subscribeToSessionStorage function for the useSyncExternalStore call, preserving
sessionStorage change notifications and the existing snapshot behavior. Keep
notification SSE functionality in the existing notification API module instead.
In `@jobdri/src/components/common/lnb/Lnb.tsx`:
- Line 176: Remove or minimize the full notification payload logging in Lnb.tsx
at jobdri/src/components/common/lnb/Lnb.tsx:176-176, retaining only the
notification type or identifier if needed. In LnbNotification.tsx at
jobdri/src/components/common/lnb/LnbNotification.tsx:426-426, remove the
click-time full-object log or gate it behind a development-only check.
- Around line 112-131: Move the toast side effect out of the
setNotificationItems updater in fetchAndUpdateNotifications. Track the
previously seen latest notification id with a latestNotificationIdRef ref,
compare it with mappedItems[0] outside the state setter, trigger the toast only
for a new unread latest item, then update the ref and set mappedItems without
side effects.
In `@jobdri/src/components/common/lnb/LnbNotification.tsx`:
- Around line 443-457: The JOB_POSTING_ASYNC_SUCCEEDED deferred-save and
JOB_POSTING_ASYNC_FAILED paths use error query values that do not match the
consumer’s modal trigger. Define or reuse one shared analysisError constant for
the not-saved case, update both router.push calls in LnbNotification
accordingly, and ensure the /mockApply/job/create consumer uses the same
constant.
---
Nitpick comments:
In
`@jobdri/src/app/mockApply/`[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx:
- Around line 269-298: Resolve the orphaned handleRetry function in the
ResumeAnalysisLoadingPageClient render flow: either remove handleRetry and its
retry state usage if all error states intentionally exit through
moveBackToResume, or wire it as a secondary action only for recoverable
transient polling failures while preserving the single-action modal for the
isError/timeout case.
In `@jobdri/src/components/common/lnb/Lnb.tsx`:
- Around line 168-169: Update the toast state flow in Lnb to store the
setTimeout handle in a ref, clear any existing timer before scheduling a new
dismissal, and clear the pending timer during effect cleanup on unmount.
Preserve the current 3000ms dismissal behavior while preventing older
notifications from dismissing newer toasts.
- Around line 108-110: The hasSubscribedRef guard in the Lnb useEffect does not
prevent Strict Mode re-subscription because cleanup resets it. Remove the ref
guard and its reset, relying on the empty dependency array for one subscription
per mount, or move the guard to module scope only if a single stream must be
shared globally.
In `@jobdri/src/components/common/lnb/LnbNotification.tsx`:
- Around line 428-436: Remove taskId from the notificationItem destructuring in
LnbNotification so only fields used by the switch and surrounding logic remain.
In `@jobdri/src/lib/api/notification.ts`:
- Around line 91-95: Remove the commented-out console.log debug block near the
SSE event handling code, leaving the surrounding notification logic unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b463a8ba-284f-4e1c-88ee-bb7325701aac
📒 Files selected for processing (10)
jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsxjobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsxjobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/page.tsxjobdri/src/app/mockApply/job/[jobPostingId]/review/page.tsxjobdri/src/app/mockApply/job/create/page.tsxjobdri/src/app/mockApply/job/loading/page.tsxjobdri/src/app/page.tsxjobdri/src/components/common/lnb/Lnb.tsxjobdri/src/components/common/lnb/LnbNotification.tsxjobdri/src/lib/api/notification.ts
| export function subscribeToNotificationStream( | ||
| onMessage: (data: ApiNotificationItem) => void, | ||
| onError?: (error: unknown) => void, | ||
| ) { | ||
| const headers = getAuthHeaders(); | ||
| const ctrl = new AbortController(); | ||
|
|
||
| if (!headers.Authorization) { | ||
| console.warn("로그인 토큰이 없어 실시간 알림을 연결하지 않습니다."); | ||
| return () => ctrl.abort(); | ||
| } | ||
|
|
||
| window.addEventListener("storage", handleStorage); | ||
| const tokenOnly = headers.Authorization.replace("Bearer ", ""); | ||
|
|
||
| return () => { | ||
| window.removeEventListener("storage", handleStorage); | ||
| }; | ||
| console.log( | ||
| "🔌 SSE 연결 시도 중... URL:", | ||
| `${API_BASE_URL}/api/notifications/stream`, | ||
| ); | ||
|
|
||
| fetchEventSource( | ||
| `${API_BASE_URL}/api/notifications/stream?accessToken=${tokenOnly}`, | ||
| { | ||
| method: "GET", | ||
| headers: { | ||
| ...headers, | ||
| Accept: "text/event-stream", | ||
| }, | ||
| signal: ctrl.signal, | ||
|
|
||
| onopen(response) { | ||
| if (response.ok) { | ||
| console.log("🟢 SSE 연결 성공!! 서버와 스트림 연결됨."); | ||
| } else { | ||
| console.error( | ||
| "🔴 SSE 연결 응답 이상:", | ||
| response.status, | ||
| response.statusText, | ||
| ); | ||
| } | ||
| }, | ||
|
|
||
| onmessage(event) { | ||
| console.log("📥 SSE raw 메시지 수신:", event.data); // 👈 서버가 뭐라도 보내면 무조건 찍힘! | ||
|
|
||
| if (!event.data || !event.data.trim().startsWith("{")) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const parsedData = JSON.parse(event.data) as ApiNotificationItem; | ||
| onMessage(parsedData); | ||
| } catch (e: unknown) { | ||
| console.error("SSE 데이터 파싱 실패:", e); | ||
| } | ||
| }, | ||
|
|
||
| onerror(err: unknown) { | ||
| console.error("❌ SSE 스트림 에러 발생:", err); | ||
| if (onError) onError(err); | ||
| }, | ||
| }, | ||
| ).catch((err) => { | ||
| console.error("❌ SSE 연결 래퍼 예외 발생:", err); | ||
| }); | ||
|
|
||
| return () => ctrl.abort(); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f 'page.tsx' -p 'jd-review' --exec ast-grep outline {} --items all
rg -nP '\bsubscribeToSessionStorage\b' --type=ts --type=tsx
rg -nP '\b(getAuthHeaders|API_BASE_URL|fetchEventSource|ApiNotificationItem)\b' -g '**/jd-review/page.tsx'Repository: JobDri-Developer/FrontEnd
Length of output: 288
🏁 Script executed:
#!/bin/bash
set -u
echo "== candidate file =="
git ls-files | grep -F 'jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx' || true
echo "== file excerpt =="
cat -n 'jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx' | sed -n '1,120p'
echo "--- line 230-260 ---"
cat -n 'jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx' | sed -n '230,260p'
echo "== references =="
git ls-files | grep -E '\.(ts|tsx)$' | xargs grep -n 'subscribeToSessionStorage' 2>/dev/null || true
echo "== target identifiers in jd-review page =="
git ls-files | grep '/jd-review/page.tsx$' | xargs grep -n 'getAuthHeaders\|API_BASE_URL\|fetchEventSource\|ApiNotificationItem' 2>/dev/null || true
echo "== notification helper excerpt =="
cat -n jobdri/src/lib/api/notification.ts | sed -n '1,150p'Repository: JobDri-Developer/FrontEnd
Length of output: 10443
Remove the accidentally pasted SSE helper from this page and restore subscribeToSessionStorage.
page.tsx now exports subscribeToNotificationStream, which references undefined getAuthHeaders, API_BASE_URL, fetchEventSource, and ApiNotificationItem; meanwhile useSyncExternalStore at line 247 still calls the missing subscribeToSessionStorage. Keep the notification SSE helper in jobdri/src/lib/api/notification.ts and restore the local session-storage subscribe helper here, unless this page intentionally needs that SSE behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/app/mockApply/`[mockApplyId]/(jd)/jd-review/page.tsx around lines
31 - 97, Remove the accidentally added subscribeToNotificationStream helper and
its SSE-related imports or references from this page. Restore a local
subscribeToSessionStorage function for the useSyncExternalStore call, preserving
sessionStorage change notifications and the existing snapshot behavior. Keep
notification SSE functionality in the existing notification API module instead.
| const fetchAndUpdateNotifications = async () => { | ||
| try { | ||
| const data = await fetchNotifications(); | ||
| if (data.isSuccess && data.result) { | ||
| const mappedItems = data.result.map(mapApiToLnbItem); | ||
| setNotificationItems(mappedItems); | ||
|
|
||
| setNotificationItems((prevItems) => { | ||
| if (prevItems.length > 0 && mappedItems.length > 0) { | ||
| const latestNewItem = mappedItems[0]; | ||
| const prevLatestItem = prevItems[0]; | ||
|
|
||
| if ( | ||
| latestNewItem.id !== prevLatestItem.id && | ||
| !latestNewItem.read | ||
| ) { | ||
| triggerToastBasedOnNotification(latestNewItem); | ||
| } | ||
| } | ||
| return mappedItems; | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Don't trigger side effects inside the setNotificationItems updater.
triggerToastBasedOnNotification calls setToastState and setTimeout from within the updater function. React may invoke updaters more than once (Strict Mode, re-entrancy), producing duplicate toasts and stray timers. Compute the diff outside the updater using the mapped result and a ref holding the previously seen latest id.
♻️ Proposed restructure
- setNotificationItems((prevItems) => {
- if (prevItems.length > 0 && mappedItems.length > 0) {
- const latestNewItem = mappedItems[0];
- const prevLatestItem = prevItems[0];
-
- if (
- latestNewItem.id !== prevLatestItem.id &&
- !latestNewItem.read
- ) {
- triggerToastBasedOnNotification(latestNewItem);
- }
- }
- return mappedItems;
- });
+ const latestNewItem = mappedItems[0];
+ const prevLatestId = latestNotificationIdRef.current;
+
+ setNotificationItems(mappedItems);
+
+ if (
+ latestNewItem &&
+ prevLatestId !== null &&
+ latestNewItem.id !== prevLatestId &&
+ !latestNewItem.read
+ ) {
+ triggerToastBasedOnNotification(latestNewItem);
+ }
+
+ latestNotificationIdRef.current = latestNewItem?.id ?? null;Add alongside the other refs:
const latestNotificationIdRef = useRef<string | null>(null);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const fetchAndUpdateNotifications = async () => { | |
| try { | |
| const data = await fetchNotifications(); | |
| if (data.isSuccess && data.result) { | |
| const mappedItems = data.result.map(mapApiToLnbItem); | |
| setNotificationItems(mappedItems); | |
| setNotificationItems((prevItems) => { | |
| if (prevItems.length > 0 && mappedItems.length > 0) { | |
| const latestNewItem = mappedItems[0]; | |
| const prevLatestItem = prevItems[0]; | |
| if ( | |
| latestNewItem.id !== prevLatestItem.id && | |
| !latestNewItem.read | |
| ) { | |
| triggerToastBasedOnNotification(latestNewItem); | |
| } | |
| } | |
| return mappedItems; | |
| }); | |
| const fetchAndUpdateNotifications = async () => { | |
| try { | |
| const data = await fetchNotifications(); | |
| if (data.isSuccess && data.result) { | |
| const mappedItems = data.result.map(mapApiToLnbItem); | |
| const latestNewItem = mappedItems[0]; | |
| const prevLatestId = latestNotificationIdRef.current; | |
| setNotificationItems(mappedItems); | |
| if ( | |
| latestNewItem && | |
| prevLatestId !== null && | |
| latestNewItem.id !== prevLatestId && | |
| !latestNewItem.read | |
| ) { | |
| triggerToastBasedOnNotification(latestNewItem); | |
| } | |
| latestNotificationIdRef.current = latestNewItem?.id ?? null; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/components/common/lnb/Lnb.tsx` around lines 112 - 131, Move the
toast side effect out of the setNotificationItems updater in
fetchAndUpdateNotifications. Track the previously seen latest notification id
with a latestNotificationIdRef ref, compare it with mappedItems[0] outside the
state setter, trigger the toast only for a new unread latest item, then update
the ref and set mappedItems without side effects.
| const mappedNewItem = mapApiToLnbItem(newNotification); | ||
| setNotificationItems((prev) => [mappedNewItem, ...prev]); | ||
| setHasNotification(true); | ||
| console.log("🔥 [SSE 수신 완료] 서버에서 알림 옴!!!", newNotification); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Debug logging of full notification payloads in both LNB files. Shared root cause: raw notification objects containing user-scoped identifiers (mockApplyId, jobPostingId, taskId) are dumped to the console on the production path.
jobdri/src/components/common/lnb/Lnb.tsx#L176-L176: remove the SSE receive log or reduce it to the notification type/id.jobdri/src/components/common/lnb/LnbNotification.tsx#L426-L426: remove the click-time full-object log or gate it behind a development check.
📍 Affects 2 files
jobdri/src/components/common/lnb/Lnb.tsx#L176-L176(this comment)jobdri/src/components/common/lnb/LnbNotification.tsx#L426-L426
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/components/common/lnb/Lnb.tsx` at line 176, Remove or minimize the
full notification payload logging in Lnb.tsx at
jobdri/src/components/common/lnb/Lnb.tsx:176-176, retaining only the
notification type or identifier if needed. In LnbNotification.tsx at
jobdri/src/components/common/lnb/LnbNotification.tsx:426-426, remove the
click-time full-object log or gate it behind a development-only check.
| switch (apiType) { | ||
| // #0 공고 분석 성공 | ||
| case "JOB_POSTING_ASYNC_SUCCEEDED": | ||
| router.push(`/job-posting/${mockApplyId}`); | ||
| if (savedToDatabase === true && jobPostingId) { | ||
| router.push(`/mockApply/job/${jobPostingId}/review`); | ||
| } else { | ||
| console.warn("⚠️ 공고 분석은 완료되었으나 DB 저장이 보류되었습니다."); | ||
| router.push(`/mockApply/job/create?analysisError=deferred`); | ||
| } | ||
| break; | ||
|
|
||
| // #1 공고 분석 실패 | ||
| case "JOB_POSTING_ASYNC_FAILED": | ||
| router.push(`/job-posting/${mockApplyId}`); | ||
| router.push(`/mockApply/job/create?analysisError=1`); | ||
| break; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f 'page.tsx' -p 'mockApply/job/create' --exec rg -nP -C4 'analysisError'
rg -nP -C2 "analysisError" --type=ts --type=tsxRepository: JobDri-Developer/FrontEnd
Length of output: 299
🏁 Script executed:
#!/bin/bash
set -u
echo "## repo root"
pwd
echo "## candidate create files"
git ls-files | grep -E '(^|/)mockApply/.*/(create|page\.(jsx?|tsx))$|(^|/)LnbNotification\.tsx$|(^|/)Lnb\.tsx$' | sed -n '1,120p'
echo "## broad search analysisError (all tracked)"
rg -n -C2 'analysisError|JOB_POSTING_ASYNC_SUCCEEDED|JOB_POSTING_ASYNC_FAILED|deferred|not_saved' . || trueRepository: JobDri-Developer/FrontEnd
Length of output: 5559
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## create page relevant section"
sed -n '85,125p' jobdri/src/app/mockApply/job/create/page.tsx | cat -n
echo "## notify component relevant section"
sed -n '438,458p' jobdri/src/components/common/lnb/LnbNotification.tsx | cat -n
echo "## loading fallback relevant section"
sed -n '145,180p' jobdri/src/app/mockApply/job/loading/page.tsx | cat -nRepository: JobDri-Developer/FrontEnd
Length of output: 3911
Use a shared analysisError constant and match the deferred DB-save path.
/mockApply/job/create explicitly opens the invalid-data modal only for analysisError=not_saved, but LnbNotification pushes analysisError=deferred and analysisError=1, so users with JOB_POSTING_ASYNC_SUCCEEDED in non-DB-saved state won’t see the modal. Align these values in one shared constant across producer and consumer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/components/common/lnb/LnbNotification.tsx` around lines 443 - 457,
The JOB_POSTING_ASYNC_SUCCEEDED deferred-save and JOB_POSTING_ASYNC_FAILED paths
use error query values that do not match the consumer’s modal trigger. Define or
reuse one shared analysisError constant for the not-saved case, update both
router.push calls in LnbNotification accordingly, and ensure the
/mockApply/job/create consumer uses the same constant.
🔗 관련 이슈
#88
📝 개요
⌨️ 작업 상세 내용
1. 공고 입력 및 분석 에러 예외 처리
백엔드 분석 데이터 누락/실패 방어: AI 분석 결과나 jobPostingId가 유실되었거나 정상 저장되지 않았을 경우, 엉뚱한 리뷰 페이지로 라우팅되지 않고 생성 페이지로 리다이렉트되도록 방어 로직 추가
사용자 안내 모달 도입: analysisError=not_saved 파라미터를 감지하여, 유저가 빈 결과 화면 대신 "자세하게 다시 작성해 주세요"라는 안내 모달(단일 확인 버튼)을 마주하도록 예외 처리 구현
2. 자소서 분석 로딩 및 실패 예외 처리
로딩 화면 유지형 에러 모달: 자소서 분석 중 타임아웃이나 에러 발생 시, 페이지를 이탈하지 않고 로딩 화면 위에 블러 오버레이와 함께 "나중에 다시 시도해주세요" 에러 모달이 고정되어 노출되도록 개선
불필요한 폴링 차단: 에러 상태(error=true)로 진입한 경우 백그라운드 API 폴링(상태 조회) 요청이 실행되지 않도록 차단 로직 추가
3. 알림(LNB) 라우팅 및 연동 보완
알림 연동 타입 분기: 공고 및 자소서 분석의 성공/실패(JOB_POSTING_ASYNC_FAILED, ANALYSIS_ASYNC_FAILED 등) 상태에 맞추어 올바른 에러 쿼리 파라미터(error=true, analysisError)와 함께 적절한 페이지로 라우팅되도록 연동
파라미터 누락 방지: 알림 클릭 시 필요한 jobPostingId 및 식별 값들이 올바르게 전달되도록 라우팅 파라미터 구조 정비
💡 코드 설명 및 참고사항
📸 스크린샷 (UI 변경 시)
🔍 리뷰 요구사항 (Reviewers)
Summary by CodeRabbit
New Features
Bug Fixes